/**
 * Schema.js
 *
 * Released under LGPL License.
 * Copyright (c) 1999-2017 Ephox Corp. All rights reserved
 *
 * License: http://www.tinymce.com/license
 * Contributing: http://www.tinymce.com/contributing
 */

/**
 * Schema validator class.
 *
 * @class tinymce.html.Schema
 * @example
 *  if (tinymce.activeEditor.schema.isValidChild('p', 'span'))
 *    alert('span is valid child of p.');
 *
 *  if (tinymce.activeEditor.schema.getElementRule('p'))
 *    alert('P is a valid element.');
 *
 * @class tinymce.html.Schema
 * @version 3.4
 */
define(
  'tinymce.core.html.Schema',
  [
    "tinymce.core.util.Tools"
  ],
  function (Tools) {
    var mapCache = {}, dummyObj = {};
    var makeMap = Tools.makeMap, each = Tools.each, extend = Tools.extend, explode = Tools.explode, inArray = Tools.inArray;

    function split(items, delim) {
      items = Tools.trim(items);
      return items ? items.split(delim || ' ') : [];
    }

    /**
     * Builds a schema lookup table
     *
     * @private
     * @param {String} type html4, html5 or html5-strict schema type.
     * @return {Object} Schema lookup table.
     */
    function compileSchema(type) {
      var schema = {}, globalAttributes, blockContent;
      var phrasingContent, flowContent, html4BlockContent, html4PhrasingContent;

      function add(name, attributes, children) {
        var ni, attributesOrder, element;

        function arrayToMap(array, obj) {
          var map = {}, i, l;

          for (i = 0, l = array.length; i < l; i++) {
            map[array[i]] = obj || {};
          }

          return map;
        }

        children = children || [];
        attributes = attributes || "";

        if (typeof children === "string") {
          children = split(children);
        }

        name = split(name);
        ni = name.length;
        while (ni--) {
          attributesOrder = split([globalAttributes, attributes].join(' '));

          element = {
            attributes: arrayToMap(attributesOrder),
            attributesOrder: attributesOrder,
            children: arrayToMap(children, dummyObj)
          };

          schema[name[ni]] = element;
        }
      }

      function addAttrs(name, attributes) {
        var ni, schemaItem, i, l;

        name = split(name);
        ni = name.length;
        attributes = split(attributes);
        while (ni--) {
          schemaItem = schema[name[ni]];
          for (i = 0, l = attributes.length; i < l; i++) {
            schemaItem.attributes[attributes[i]] = {};
            schemaItem.attributesOrder.push(attributes[i]);
          }
        }
      }

      // Use cached schema
      if (mapCache[type]) {
        return mapCache[type];
      }

      // Attributes present on all elements
      globalAttributes = "id accesskey class dir lang style tabindex title role";

      // Event attributes can be opt-in/opt-out
      /*eventAttributes = split("onabort onblur oncancel oncanplay oncanplaythrough onchange onclick onclose oncontextmenu oncuechange " +
       "ondblclick ondrag ondragend ondragenter ondragleave ondragover ondragstart ondrop ondurationchange onemptied onended " +
       "onerror onfocus oninput oninvalid onkeydown onkeypress onkeyup onload onloadeddata onloadedmetadata onloadstart " +
       "onmousedown onmousemove onmouseout onmouseover onmouseup onmousewheel onpause onplay onplaying onprogress onratechange " +
       "onreset onscroll onseeked onseeking onseeking onselect onshow onstalled onsubmit onsuspend ontimeupdate onvolumechange " +
       "onwaiting"
       );*/

      // Block content elements
      blockContent =
        "address blockquote div dl fieldset form h1 h2 h3 h4 h5 h6 hr menu ol p pre table ul";

      // Phrasing content elements from the HTML5 spec (inline)
      phrasingContent =
        "a abbr b bdo br button cite code del dfn em embed i iframe img input ins kbd " +
        "label map noscript object q s samp script select small span strong sub sup " +
        "textarea u var #text #comment"
        ;

      // Add HTML5 items to globalAttributes, blockContent, phrasingContent
      if (type != "html4") {
        globalAttributes += " contenteditable contextmenu draggable dropzone " +
          "hidden spellcheck translate";
        blockContent += " article aside details dialog figure header footer hgroup section nav";
        phrasingContent += " audio canvas command datalist mark meter output picture " +
          "progress time wbr video ruby bdi keygen";
      }

      // Add HTML4 elements unless it's html5-strict
      if (type != "html5-strict") {
        globalAttributes += " xml:lang";

        html4PhrasingContent = "acronym applet basefont big font strike tt";
        phrasingContent = [phrasingContent, html4PhrasingContent].join(' ');

        each(split(html4PhrasingContent), function (name) {
          add(name, "", phrasingContent);
        });

        html4BlockContent = "center dir isindex noframes";
        blockContent = [blockContent, html4BlockContent].join(' ');

        // Flow content elements from the HTML5 spec (block+inline)
        flowContent = [blockContent, phrasingContent].join(' ');

        each(split(html4BlockContent), function (name) {
          add(name, "", flowContent);
        });
      }

      // Flow content elements from the HTML5 spec (block+inline)
      flowContent = flowContent || [blockContent, phrasingContent].join(" ");

      // HTML4 base schema TODO: Move HTML5 specific attributes to HTML5 specific if statement
      // Schema items <element name>, <specific attributes>, <children ..>
      add("html", "manifest", "head body");
      add("head", "", "base command link meta noscript script style title");
      add("title hr noscript br");
      add("base", "href target");
      add("link", "href rel media hreflang type sizes hreflang");
      add("meta", "name http-equiv content charset");
      add("style", "media type scoped");
      add("script", "src async defer type charset");
      add("body", "onafterprint onbeforeprint onbeforeunload onblur onerror onfocus " +
        "onhashchange onload onmessage onoffline ononline onpagehide onpageshow " +
        "onpopstate onresize onscroll onstorage onunload", flowContent);
      add("address dt dd div caption", "", flowContent);
      add("h1 h2 h3 h4 h5 h6 pre p abbr code var samp kbd sub sup i b u bdo span legend em strong small s cite dfn", "", phrasingContent);
      add("blockquote", "cite", flowContent);
      add("ol", "reversed start type", "li");
      add("ul", "", "li");
      add("li", "value", flowContent);
      add("dl", "", "dt dd");
      add("a", "href target rel media hreflang type", phrasingContent);
      add("q", "cite", phrasingContent);
      add("ins del", "cite datetime", flowContent);
      add("img", "src sizes srcset alt usemap ismap width height");
      add("iframe", "src name width height", flowContent);
      add("embed", "src type width height");
      add("object", "data type typemustmatch name usemap form width height", [flowContent, "param"].join(' '));
      add("param", "name value");
      add("map", "name", [flowContent, "area"].join(' '));
      add("area", "alt coords shape href target rel media hreflang type");
      add("table", "border", "caption colgroup thead tfoot tbody tr" + (type == "html4" ? " col" : ""));
      add("colgroup", "span", "col");
      add("col", "span");
      add("tbody thead tfoot", "", "tr");
      add("tr", "", "td th");
      add("td", "colspan rowspan headers", flowContent);
      add("th", "colspan rowspan headers scope abbr", flowContent);
      add("form", "accept-charset action autocomplete enctype method name novalidate target", flowContent);
      add("fieldset", "disabled form name", [flowContent, "legend"].join(' '));
      add("label", "form for", phrasingContent);
      add("input", "accept alt autocomplete checked dirname disabled form formaction formenctype formmethod formnovalidate " +
        "formtarget height list max maxlength min multiple name pattern readonly required size src step type value width"
      );
      add("button", "disabled form formaction formenctype formmethod formnovalidate formtarget name type value",
        type == "html4" ? flowContent : phrasingContent);
      add("select", "disabled form multiple name required size", "option optgroup");
      add("optgroup", "disabled label", "option");
      add("option", "disabled label selected value");
      add("textarea", "cols dirname disabled form maxlength name readonly required rows wrap");
      add("menu", "type label", [flowContent, "li"].join(' '));
      add("noscript", "", flowContent);

      // Extend with HTML5 elements
      if (type != "html4") {
        add("wbr");
        add("ruby", "", [phrasingContent, "rt rp"].join(' '));
        add("figcaption", "", flowContent);
        add("mark rt rp summary bdi", "", phrasingContent);
        add("canvas", "width height", flowContent);
        add("video", "src crossorigin poster preload autoplay mediagroup loop " +
          "muted controls width height buffered", [flowContent, "track source"].join(' '));
        add("audio", "src crossorigin preload autoplay mediagroup loop muted controls " +
          "buffered volume", [flowContent, "track source"].join(' '));
        add("picture", "", "img source");
        add("source", "src srcset type media sizes");
        add("track", "kind src srclang label default");
        add("datalist", "", [phrasingContent, "option"].join(' '));
        add("article section nav aside header footer", "", flowContent);
        add("hgroup", "", "h1 h2 h3 h4 h5 h6");
        add("figure", "", [flowContent, "figcaption"].join(' '));
        add("time", "datetime", phrasingContent);
        add("dialog", "open", flowContent);
        add("command", "type label icon disabled checked radiogroup command");
        add("output", "for form name", phrasingContent);
        add("progress", "value max", phrasingContent);
        add("meter", "value min max low high optimum", phrasingContent);
        add("details", "open", [flowContent, "summary"].join(' '));
        add("keygen", "autofocus challenge disabled form keytype name");
      }

      // Extend with HTML4 attributes unless it's html5-strict
      if (type != "html5-strict") {
        addAttrs("script", "language xml:space");
        addAttrs("style", "xml:space");
        addAttrs("object", "declare classid code codebase codetype archive standby align border hspace vspace");
        addAttrs("embed", "align name hspace vspace");
        addAttrs("param", "valuetype type");
        addAttrs("a", "charset name rev shape coords");
        addAttrs("br", "clear");
        addAttrs("applet", "codebase archive code object alt name width height align hspace vspace");
        addAttrs("img", "name longdesc align border hspace vspace");
        addAttrs("iframe", "longdesc frameborder marginwidth marginheight scrolling align");
        addAttrs("font basefont", "size color face");
        addAttrs("input", "usemap align");
        addAttrs("select", "onchange");
        addAttrs("textarea");
        addAttrs("h1 h2 h3 h4 h5 h6 div p legend caption", "align");
        addAttrs("ul", "type compact");
        addAttrs("li", "type");
        addAttrs("ol dl menu dir", "compact");
        addAttrs("pre", "width xml:space");
        addAttrs("hr", "align noshade size width");
        addAttrs("isindex", "prompt");
        addAttrs("table", "summary width frame rules cellspacing cellpadding align bgcolor");
        addAttrs("col", "width align char charoff valign");
        addAttrs("colgroup", "width align char charoff valign");
        addAttrs("thead", "align char charoff valign");
        addAttrs("tr", "align char charoff valign bgcolor");
        addAttrs("th", "axis align char charoff valign nowrap bgcolor width height");
        addAttrs("form", "accept");
        addAttrs("td", "abbr axis scope align char charoff valign nowrap bgcolor width height");
        addAttrs("tfoot", "align char charoff valign");
        addAttrs("tbody", "align char charoff valign");
        addAttrs("area", "nohref");
        addAttrs("body", "background bgcolor text link vlink alink");
      }

      // Extend with HTML5 attributes unless it's html4
      if (type != "html4") {
        addAttrs("input button select textarea", "autofocus");
        addAttrs("input textarea", "placeholder");
        addAttrs("a", "download");
        addAttrs("link script img", "crossorigin");
        addAttrs("iframe", "sandbox seamless allowfullscreen"); // Excluded: srcdoc
      }

      // Special: iframe, ruby, video, audio, label

      // Delete children of the same name from it's parent
      // For example: form can't have a child of the name form
      each(split('a form meter progress dfn'), function (name) {
        if (schema[name]) {
          delete schema[name].children[name];
        }
      });

      // Delete header, footer, sectioning and heading content descendants
      /*each('dt th address', function(name) {
       delete schema[name].children[name];
       });*/

      // Caption can't have tables
      delete schema.caption.children.table;

      // Delete scripts by default due to possible XSS
      delete schema.script;

      // TODO: LI:s can only have value if parent is OL

      // TODO: Handle transparent elements
      // a ins del canvas map

      mapCache[type] = schema;

      return schema;
    }

    function compileElementMap(value, mode) {
      var styles;

      if (value) {
        styles = {};

        if (typeof value == 'string') {
          value = {
            '*': value
          };
        }

        // Convert styles into a rule list
        each(value, function (value, key) {
          styles[key] = styles[key.toUpperCase()] = mode == 'map' ? makeMap(value, /[, ]/) : explode(value, /[, ]/);
        });
      }

      return styles;
    }

    /**
     * Constructs a new Schema instance.
     *
     * @constructor
     * @method Schema
     * @param {Object} settings Name/value settings object.
     */
    return function (settings) {
      var self = this, elements = {}, children = {}, patternElements = [], validStyles, invalidStyles, schemaItems;
      var whiteSpaceElementsMap, selfClosingElementsMap, shortEndedElementsMap, boolAttrMap, validClasses;
      var blockElementsMap, nonEmptyElementsMap, moveCaretBeforeOnEnterElementsMap, textBlockElementsMap, textInlineElementsMap;
      var customElementsMap = {}, specialElements = {};

      // Creates an lookup table map object for the specified option or the default value
      function createLookupTable(option, defaultValue, extendWith) {
        var value = settings[option];

        if (!value) {
          // Get cached default map or make it if needed
          value = mapCache[option];

          if (!value) {
            value = makeMap(defaultValue, ' ', makeMap(defaultValue.toUpperCase(), ' '));
            value = extend(value, extendWith);

            mapCache[option] = value;
          }
        } else {
          // Create custom map
          value = makeMap(value, /[, ]/, makeMap(value.toUpperCase(), /[, ]/));
        }

        return value;
      }

      settings = settings || {};
      schemaItems = compileSchema(settings.schema);

      // Allow all elements and attributes if verify_html is set to false
      if (settings.verify_html === false) {
        settings.valid_elements = '*[*]';
      }

      validStyles = compileElementMap(settings.valid_styles);
      invalidStyles = compileElementMap(settings.invalid_styles, 'map');
      validClasses = compileElementMap(settings.valid_classes, 'map');

      // Setup map objects
      whiteSpaceElementsMap = createLookupTable(
        'whitespace_elements',
        'pre script noscript style textarea video audio iframe object code'
      );
      selfClosingElementsMap = createLookupTable('self_closing_elements', 'colgroup dd dt li option p td tfoot th thead tr');
      shortEndedElementsMap = createLookupTable('short_ended_elements', 'area base basefont br col frame hr img input isindex link ' +
        'meta param embed source wbr track');
      boolAttrMap = createLookupTable('boolean_attributes', 'checked compact declare defer disabled ismap multiple nohref noresize ' +
        'noshade nowrap readonly selected autoplay loop controls');
      nonEmptyElementsMap = createLookupTable('non_empty_elements', 'td th iframe video audio object ' +
        'script pre code', shortEndedElementsMap);
      moveCaretBeforeOnEnterElementsMap = createLookupTable('move_caret_before_on_enter_elements', 'table', nonEmptyElementsMap);
      textBlockElementsMap = createLookupTable('text_block_elements', 'h1 h2 h3 h4 h5 h6 p div address pre form ' +
        'blockquote center dir fieldset header footer article section hgroup aside nav figure');
      blockElementsMap = createLookupTable('block_elements', 'hr table tbody thead tfoot ' +
        'th tr td li ol ul caption dl dt dd noscript menu isindex option ' +
        'datalist select optgroup figcaption', textBlockElementsMap);
      textInlineElementsMap = createLookupTable('text_inline_elements', 'span strong b em i font strike u var cite ' +
        'dfn code mark q sup sub samp');

      each((settings.special || 'script noscript noframes noembed title style textarea xmp').split(' '), function (name) {
        specialElements[name] = new RegExp('<\/' + name + '[^>]*>', 'gi');
      });

      // Converts a wildcard expression string to a regexp for example *a will become /.*a/.
      function patternToRegExp(str) {
        return new RegExp('^' + str.replace(/([?+*])/g, '.$1') + '$');
      }

      // Parses the specified valid_elements string and adds to the current rules
      // This function is a bit hard to read since it's heavily optimized for speed
      function addValidElements(validElements) {
        var ei, el, ai, al, matches, element, attr, attrData, elementName, attrName, attrType, attributes, attributesOrder,
          prefix, outputName, globalAttributes, globalAttributesOrder, key, value,
          elementRuleRegExp = /^([#+\-])?([^\[!\/]+)(?:\/([^\[!]+))?(?:(!?)\[([^\]]+)\])?$/,
          attrRuleRegExp = /^([!\-])?(\w+::\w+|[^=:<]+)?(?:([=:<])(.*))?$/,
          hasPatternsRegExp = /[*?+]/;

        if (validElements) {
          // Split valid elements into an array with rules
          validElements = split(validElements, ',');

          if (elements['@']) {
            globalAttributes = elements['@'].attributes;
            globalAttributesOrder = elements['@'].attributesOrder;
          }

          // Loop all rules
          for (ei = 0, el = validElements.length; ei < el; ei++) {
            // Parse element rule
            matches = elementRuleRegExp.exec(validElements[ei]);
            if (matches) {
              // Setup local names for matches
              prefix = matches[1];
              elementName = matches[2];
              outputName = matches[3];
              attrData = matches[5];

              // Create new attributes and attributesOrder
              attributes = {};
              attributesOrder = [];

              // Create the new element
              element = {
                attributes: attributes,
                attributesOrder: attributesOrder
              };

              // Padd empty elements prefix
              if (prefix === '#') {
                element.paddEmpty = true;
              }

              // Remove empty elements prefix
              if (prefix === '-') {
                element.removeEmpty = true;
              }

              if (matches[4] === '!') {
                element.removeEmptyAttrs = true;
              }

              // Copy attributes from global rule into current rule
              if (globalAttributes) {
                for (key in globalAttributes) {
                  attributes[key] = globalAttributes[key];
                }

                attributesOrder.push.apply(attributesOrder, globalAttributesOrder);
              }

              // Attributes defined
              if (attrData) {
                attrData = split(attrData, '|');
                for (ai = 0, al = attrData.length; ai < al; ai++) {
                  matches = attrRuleRegExp.exec(attrData[ai]);
                  if (matches) {
                    attr = {};
                    attrType = matches[1];
                    attrName = matches[2].replace(/::/g, ':');
                    prefix = matches[3];
                    value = matches[4];

                    // Required
                    if (attrType === '!') {
                      element.attributesRequired = element.attributesRequired || [];
                      element.attributesRequired.push(attrName);
                      attr.required = true;
                    }

                    // Denied from global
                    if (attrType === '-') {
                      delete attributes[attrName];
                      attributesOrder.splice(inArray(attributesOrder, attrName), 1);
                      continue;
                    }

                    // Default value
                    if (prefix) {
                      // Default value
                      if (prefix === '=') {
                        element.attributesDefault = element.attributesDefault || [];
                        element.attributesDefault.push({ name: attrName, value: value });
                        attr.defaultValue = value;
                      }

                      // Forced value
                      if (prefix === ':') {
                        element.attributesForced = element.attributesForced || [];
                        element.attributesForced.push({ name: attrName, value: value });
                        attr.forcedValue = value;
                      }

                      // Required values
                      if (prefix === '<') {
                        attr.validValues = makeMap(value, '?');
                      }
                    }

                    // Check for attribute patterns
                    if (hasPatternsRegExp.test(attrName)) {
                      element.attributePatterns = element.attributePatterns || [];
                      attr.pattern = patternToRegExp(attrName);
                      element.attributePatterns.push(attr);
                    } else {
                      // Add attribute to order list if it doesn't already exist
                      if (!attributes[attrName]) {
                        attributesOrder.push(attrName);
                      }

                      attributes[attrName] = attr;
                    }
                  }
                }
              }

              // Global rule, store away these for later usage
              if (!globalAttributes && elementName == '@') {
                globalAttributes = attributes;
                globalAttributesOrder = attributesOrder;
              }

              // Handle substitute elements such as b/strong
              if (outputName) {
                element.outputName = elementName;
                elements[outputName] = element;
              }

              // Add pattern or exact element
              if (hasPatternsRegExp.test(elementName)) {
                element.pattern = patternToRegExp(elementName);
                patternElements.push(element);
              } else {
                elements[elementName] = element;
              }
            }
          }
        }
      }

      function setValidElements(validElements) {
        elements = {};
        patternElements = [];

        addValidElements(validElements);

        each(schemaItems, function (element, name) {
          children[name] = element.children;
        });
      }

      // Adds custom non HTML elements to the schema
      function addCustomElements(customElements) {
        var customElementRegExp = /^(~)?(.+)$/;

        if (customElements) {
          // Flush cached items since we are altering the default maps
          mapCache.text_block_elements = mapCache.block_elements = null;

          each(split(customElements, ','), function (rule) {
            var matches = customElementRegExp.exec(rule),
              inline = matches[1] === '~',
              cloneName = inline ? 'span' : 'div',
              name = matches[2];

            children[name] = children[cloneName];
            customElementsMap[name] = cloneName;

            // If it's not marked as inline then add it to valid block elements
            if (!inline) {
              blockElementsMap[name.toUpperCase()] = {};
              blockElementsMap[name] = {};
            }

            // Add elements clone if needed
            if (!elements[name]) {
              var customRule = elements[cloneName];

              customRule = extend({}, customRule);
              delete customRule.removeEmptyAttrs;
              delete customRule.removeEmpty;

              elements[name] = customRule;
            }

            // Add custom elements at span/div positions
            each(children, function (element, elmName) {
              if (element[cloneName]) {
                children[elmName] = element = extend({}, children[elmName]);
                element[name] = element[cloneName];
              }
            });
          });
        }
      }

      // Adds valid children to the schema object
      function addValidChildren(validChildren) {
        var childRuleRegExp = /^([+\-]?)(\w+)\[([^\]]+)\]$/;

        // Invalidate the schema cache if the schema is mutated
        mapCache[settings.schema] = null;

        if (validChildren) {
          each(split(validChildren, ','), function (rule) {
            var matches = childRuleRegExp.exec(rule), parent, prefix;

            if (matches) {
              prefix = matches[1];

              // Add/remove items from default
              if (prefix) {
                parent = children[matches[2]];
              } else {
                parent = children[matches[2]] = { '#comment': {} };
              }

              parent = children[matches[2]];

              each(split(matches[3], '|'), function (child) {
                if (prefix === '-') {
                  delete parent[child];
                } else {
                  parent[child] = {};
                }
              });
            }
          });
        }
      }

      function getElementRule(name) {
        var element = elements[name], i;

        // Exact match found
        if (element) {
          return element;
        }

        // No exact match then try the patterns
        i = patternElements.length;
        while (i--) {
          element = patternElements[i];

          if (element.pattern.test(name)) {
            return element;
          }
        }
      }

      if (!settings.valid_elements) {
        // No valid elements defined then clone the elements from the schema spec
        each(schemaItems, function (element, name) {
          elements[name] = {
            attributes: element.attributes,
            attributesOrder: element.attributesOrder
          };

          children[name] = element.children;
        });

        // Switch these on HTML4
        if (settings.schema != "html5") {
          each(split('strong/b em/i'), function (item) {
            item = split(item, '/');
            elements[item[1]].outputName = item[0];
          });
        }

        // Add default alt attribute for images, removed since alt="" is treated as presentational.
        // elements.img.attributesDefault = [{name: 'alt', value: ''}];

        // Remove these if they are empty by default
        each(split('ol ul sub sup blockquote span font a table tbody tr strong em b i'), function (name) {
          if (elements[name]) {
            elements[name].removeEmpty = true;
          }
        });

        // Padd these by default
        each(split('p h1 h2 h3 h4 h5 h6 th td pre div address caption'), function (name) {
          elements[name].paddEmpty = true;
        });

        // Remove these if they have no attributes
        each(split('span'), function (name) {
          elements[name].removeEmptyAttrs = true;
        });

        // Remove these by default
        // TODO: Reenable in 4.1
        /*each(split('script style'), function(name) {
         delete elements[name];
         });*/
      } else {
        setValidElements(settings.valid_elements);
      }

      addCustomElements(settings.custom_elements);
      addValidChildren(settings.valid_children);
      addValidElements(settings.extended_valid_elements);

      // Todo: Remove this when we fix list handling to be valid
      addValidChildren('+ol[ul|ol],+ul[ul|ol]');


      // Some elements are not valid by themselves - require parents
      each({
        dd: 'dl',
        dt: 'dl',
        li: 'ul ol',
        td: 'tr',
        th: 'tr',
        tr: 'tbody thead tfoot',
        tbody: 'table',
        thead: 'table',
        tfoot: 'table',
        legend: 'fieldset',
        area: 'map',
        param: 'video audio object'
      }, function (parents, item) {
        if (elements[item]) {
          elements[item].parentsRequired = split(parents);
        }
      });


      // Delete invalid elements
      if (settings.invalid_elements) {
        each(explode(settings.invalid_elements), function (item) {
          if (elements[item]) {
            delete elements[item];
          }
        });
      }

      // If the user didn't allow span only allow internal spans
      if (!getElementRule('span')) {
        addValidElements('span[!data-mce-type|*]');
      }

      /**
       * Name/value map object with valid parents and children to those parents.
       *
       * @example
       * children = {
       *    div:{p:{}, h1:{}}
       * };
       * @field children
       * @type Object
       */
      self.children = children;

      /**
       * Name/value map object with valid styles for each element.
       *
       * @method getValidStyles
       * @type Object
       */
      self.getValidStyles = function () {
        return validStyles;
      };

      /**
       * Name/value map object with valid styles for each element.
       *
       * @method getInvalidStyles
       * @type Object
       */
      self.getInvalidStyles = function () {
        return invalidStyles;
      };

      /**
       * Name/value map object with valid classes for each element.
       *
       * @method getValidClasses
       * @type Object
       */
      self.getValidClasses = function () {
        return validClasses;
      };

      /**
       * Returns a map with boolean attributes.
       *
       * @method getBoolAttrs
       * @return {Object} Name/value lookup map for boolean attributes.
       */
      self.getBoolAttrs = function () {
        return boolAttrMap;
      };

      /**
       * Returns a map with block elements.
       *
       * @method getBlockElements
       * @return {Object} Name/value lookup map for block elements.
       */
      self.getBlockElements = function () {
        return blockElementsMap;
      };

      /**
       * Returns a map with text block elements. Such as: p,h1-h6,div,address
       *
       * @method getTextBlockElements
       * @return {Object} Name/value lookup map for block elements.
       */
      self.getTextBlockElements = function () {
        return textBlockElementsMap;
      };

      /**
       * Returns a map of inline text format nodes for example strong/span or ins.
       *
       * @method getTextInlineElements
       * @return {Object} Name/value lookup map for text format elements.
       */
      self.getTextInlineElements = function () {
        return textInlineElementsMap;
      };

      /**
       * Returns a map with short ended elements such as BR or IMG.
       *
       * @method getShortEndedElements
       * @return {Object} Name/value lookup map for short ended elements.
       */
      self.getShortEndedElements = function () {
        return shortEndedElementsMap;
      };

      /**
       * Returns a map with self closing tags such as <li>.
       *
       * @method getSelfClosingElements
       * @return {Object} Name/value lookup map for self closing tags elements.
       */
      self.getSelfClosingElements = function () {
        return selfClosingElementsMap;
      };

      /**
       * Returns a map with elements that should be treated as contents regardless if it has text
       * content in them or not such as TD, VIDEO or IMG.
       *
       * @method getNonEmptyElements
       * @return {Object} Name/value lookup map for non empty elements.
       */
      self.getNonEmptyElements = function () {
        return nonEmptyElementsMap;
      };

      /**
       * Returns a map with elements that the caret should be moved in front of after enter is
       * pressed
       *
       * @method getMoveCaretBeforeOnEnterElements
       * @return {Object} Name/value lookup map for elements to place the caret in front of.
       */
      self.getMoveCaretBeforeOnEnterElements = function () {
        return moveCaretBeforeOnEnterElementsMap;
      };

      /**
       * Returns a map with elements where white space is to be preserved like PRE or SCRIPT.
       *
       * @method getWhiteSpaceElements
       * @return {Object} Name/value lookup map for white space elements.
       */
      self.getWhiteSpaceElements = function () {
        return whiteSpaceElementsMap;
      };

      /**
       * Returns a map with special elements. These are elements that needs to be parsed
       * in a special way such as script, style, textarea etc. The map object values
       * are regexps used to find the end of the element.
       *
       * @method getSpecialElements
       * @return {Object} Name/value lookup map for special elements.
       */
      self.getSpecialElements = function () {
        return specialElements;
      };

      /**
       * Returns true/false if the specified element and it's child is valid or not
       * according to the schema.
       *
       * @method isValidChild
       * @param {String} name Element name to check for.
       * @param {String} child Element child to verify.
       * @return {Boolean} True/false if the element is a valid child of the specified parent.
       */
      self.isValidChild = function (name, child) {
        var parent = children[name.toLowerCase()];

        return !!(parent && parent[child.toLowerCase()]);
      };

      /**
       * Returns true/false if the specified element name and optional attribute is
       * valid according to the schema.
       *
       * @method isValid
       * @param {String} name Name of element to check.
       * @param {String} attr Optional attribute name to check for.
       * @return {Boolean} True/false if the element and attribute is valid.
       */
      self.isValid = function (name, attr) {
        var attrPatterns, i, rule = getElementRule(name);

        // Check if it's a valid element
        if (rule) {
          if (attr) {
            // Check if attribute name exists
            if (rule.attributes[attr]) {
              return true;
            }

            // Check if attribute matches a regexp pattern
            attrPatterns = rule.attributePatterns;
            if (attrPatterns) {
              i = attrPatterns.length;
              while (i--) {
                if (attrPatterns[i].pattern.test(name)) {
                  return true;
                }
              }
            }
          } else {
            return true;
          }
        }

        // No match
        return false;
      };

      /**
       * Returns true/false if the specified element is valid or not
       * according to the schema.
       *
       * @method getElementRule
       * @param {String} name Element name to check for.
       * @return {Object} Element object or undefined if the element isn't valid.
       */
      self.getElementRule = getElementRule;

      /**
       * Returns an map object of all custom elements.
       *
       * @method getCustomElements
       * @return {Object} Name/value map object of all custom elements.
       */
      self.getCustomElements = function () {
        return customElementsMap;
      };

      /**
       * Parses a valid elements string and adds it to the schema. The valid elements
       * format is for example "element[attr=default|otherattr]".
       * Existing rules will be replaced with the ones specified, so this extends the schema.
       *
       * @method addValidElements
       * @param {String} valid_elements String in the valid elements format to be parsed.
       */
      self.addValidElements = addValidElements;

      /**
       * Parses a valid elements string and sets it to the schema. The valid elements
       * format is for example "element[attr=default|otherattr]".
       * Existing rules will be replaced with the ones specified, so this extends the schema.
       *
       * @method setValidElements
       * @param {String} valid_elements String in the valid elements format to be parsed.
       */
      self.setValidElements = setValidElements;

      /**
       * Adds custom non HTML elements to the schema.
       *
       * @method addCustomElements
       * @param {String} custom_elements Comma separated list of custom elements to add.
       */
      self.addCustomElements = addCustomElements;

      /**
       * Parses a valid children string and adds them to the schema structure. The valid children
       * format is for example: "element[child1|child2]".
       *
       * @method addValidChildren
       * @param {String} valid_children Valid children elements string to parse
       */
      self.addValidChildren = addValidChildren;

      self.elements = elements;
    };
  }
);
